#!/bin/bash
# 所有shell脚本的测试用例都应该source此文件
# 约定：
#   1、以下划线"_"开头的函数和变量用例不能直接调用
#   2、环境变量全大写，全局变量加上"g_"前置，局部变量统一加"local"修饰

export LANG="en_US.UTF-8"
export LANGUAGE="en_US:en"
export LC_ALL="en_US.UTF-8"

if [ -f "$TST_TS_TOPDIR/common/lib/common.sh" ]; then
    export TST_COMMON_TOPDIR="$TST_TS_TOPDIR/common"
    # source测试套自定义的公共文件
    source "$TST_TS_TOPDIR/lib/ts_common.sh" || exit 1
    # source全局公共文件
    source "$TST_COMMON_TOPDIR/lib/common_func.sh" || exit 1
elif [ -f "$TST_TS_TOPDIR/lib/common.sh" ]; then
    export TST_COMMON_TOPDIR="$TST_TS_TOPDIR"
    # 只是tst_suite_common使用
    source "$TST_TS_TOPDIR/lib/common_func.sh" || exit 1
else
    echo "can't find the tsuite common file"
    exit 1
fi

# 环境变量
# TST_TS_TOPDIR 测试套顶层目录
# TST_TS_SYSDIR 测试套公共运行目录
[ -z "$TST_TS_SYSDIR" ] && export TST_TS_SYSDIR="$TST_TS_TOPDIR/logs/.ts.sysdir"
# TST_TC_SYSDIR 测试用例管理用临时目录
# TST_TC_CWD 测试用例CWD
# TST_TC_PID 测试用例主进程pid
# 下面几个为用例结果状态
export TST_PASS=0
export TST_FAIL=1
export TST_INIT=2
export TST_SKIP=3
export TST_MANUAL=4
export TST_TIMEOUT=5
# TST_TC_NAME 用例名称
# TST_TC_FILE 用例文件
# TST_VM_SUBNET 虚拟机子网
if [ -z "$TST_VM_SUBNET" ]; then
    TST_VM_SUBNET=$(ifconfig eth0 2>/dev/null | grep "netmask .* broadcast 192\.168\." | awk '{print $2}')
    [ -z "$TST_VM_SUBNET" ] && TST_VM_SUBNET=$(ifconfig eth1 2>/dev/null |
        grep "netmask .* broadcast 192\.168\." | awk '{print $2}')
    [ -z "$TST_VM_SUBNET" ] && TST_VM_SUBNET=$(ifconfig eth2 2>/dev/null |
        grep "netmask .* broadcast 192\.168\." | awk '{print $2}')
    TST_VM_SUBNET=${TST_VM_SUBNET%.*}
fi
export TST_VM_SUBNET

g_tst_case_start=$(get_up_time_ms)
g_tst_trap_signal=12

_tcstat_to_str() {
    case "$1" in
        "TST_PASS" | "$TST_PASS")
            echo "TST_PASS"
            ;;
        "TST_FAIL" | "$TST_FAIL")
            echo "TST_FAIL"
            ;;
        "TST_INIT" | "$TST_INIT")
            echo "TST_INIT"
            ;;
        "TST_SKIP" | "$TST_SKIP")
            echo "TST_SKIP"
            ;;
        "TST_MANUAL" | "$TST_MANUAL")
            echo "TST_MANUAL"
            ;;
        "TST_TIMEOUT" | "$TST_TIMEOUT")
            echo "TST_TIMEOUT"
            ;;
        *)
            echo "TST_UNKNOWN"
            ;;
    esac
}

# 获取用例属性
# $1 -- 用例属性名称，例如：用例ID、用例名称
_get_case_attr() {
    grep "@${1}:" "$TST_TC_FILE_FULL" | head -n 1 | sed "s|.*@${1}:\s*||g" | sed "s|\s*$||g"
}
# 显示用例属性
_shou_case_attr() {
    local attr_list="
    用例ID
    用例名称
    用例级别
    用例标签
    扩展属性
    用例类型
    自动化
    超时时间
    用例描述
    预置条件
    "
    echo "==================== vvv 用例属性 vvv ===================="
    for case_attr in $attr_list; do
        echo "${case_attr}: $(_get_case_attr "$case_attr")"
    done
    grep "@测试步骤:\|@预期结果:" "$TST_TC_FILE_FULL" | sed "s|.*@测试步骤:|@测试步骤:|g" |
        sed "s|.*@预期结果:|@预期结果:|g" | sed "s|\s*$||g"
    echo "==================== ^^^ 用例属性 ^^^ ===================="
}

# 获取测试结果文件
_get_tst_result_file() {
    TST_RESULT_FILE="$TST_TS_TOPDIR/logs/report.result"
    export TST_RESULT_FILE
    if [ ! -f "$TST_RESULT_FILE" ]; then
        {
            echo "TESTSUITE"
            echo "suite-name: $(get_suite_name)"
            echo "suite-start-time: $(get_timestamp_ms)"
            echo "suite-end-time:"
            echo ""
        } >>"$TST_RESULT_FILE"
    fi
    echo "$TST_RESULT_FILE"
}

_assert_trap_exit() {
    /bin/kill -s "$g_tst_trap_signal" -- "$TST_TC_PID"
}

# 功能：标记用例为SKIP状态（当用例不需要测试时）
# 参数：
#   $* -- 对SKIP状态标记的描述
# 返回值：无
skip_test() {
    local _tst_tc_stat
    _tst_tc_stat="$(_get_tcstat)"

    case "$_tst_tc_stat" in
        "$TST_PASS" | "$TST_INIT" | "$TST_SKIP" | "$TST_MANUAL")
            _set_tcstat "$TST_SKIP"
            msg "set testcase SKIP: $*"
            _assert_trap_exit
            ;;
        *)
            msg "set testcase SKIP fail: $*"
            err "the testcase stat is $(_tcstat_to_str "$_tst_tc_stat"), can't set to SKIP"
            ;;
    esac
}

# 功能：当表达式返回真或命令执行成功时，用例不满足测试条件，终止测试
# 参数：
#   $* -- 需要断言的表达式
# 返回值：无
skip_if_true() {
    local _tst_ret
    "$@"
    _tst_ret=$?
    if [ $_tst_ret -eq 0 ]; then
        skip_test "skip_if_true get (_tst_ret:$_tst_ret) ->" "$@"
    fi
}

# 功能：当表达式返回假或命令执行失败，用例不满足测试条件，终止测试
# 参数：
#   $* -- 需要断言的表达式
# 返回值：无
skip_if_false() {
    local _tst_ret
    "$@"
    _tst_ret=$?
    if [ $_tst_ret -ne 0 ]; then
        skip_test "skip_if_false get (_tst_ret:$_tst_ret) ->" "$@"
    fi
}

# 功能：断言表达式返回真或命令执行成功，'_cont'后缀的断言函数在断言失败后用例继续执行，不终止
# 参数：
#   $* -- 需要断言的表达式
# 返回值：无
assert_true_cont() {
    local _tst_ret
    "$@"
    _tst_ret=$?
    if [ $_tst_ret -eq 0 ]; then
        _tc_pass
        return 0
    else
        err "assert_true, but return ${_tst_ret}: $*"
        return 1
    fi
}
assert_true() {
    assert_true_cont "$@" || _assert_trap_exit
}

# 功能：断言表达式返回假或命令执行失败，'_cont'后缀的断言函数在断言失败后用例继续执行，不终止
# 参数：
#   $* -- 需要断言的表达式
# 返回值：无
assert_false_cont() {
    local _tst_ret
    "$@"
    _tst_ret=$?
    if [ $_tst_ret -ne 0 ]; then
        _tc_pass
        return 0
    else
        err "assert_false, but return ${_tst_ret}: $*"
        return 1
    fi
}
assert_false() {
    assert_false_cont "$@" || _assert_trap_exit
}

_set_tcstat() {
    echo "$1" >"$TST_TC_SYSDIR/tcstat"
}

_get_tcstat() {
    cat "$TST_TC_SYSDIR/tcstat" 2>/dev/null
}

is_pass() {
    [ "$(_get_tcstat)" == "$TST_PASS" ]
}

_tc_pass() {
    # 只有初始状态的用例才能置为PASS，其他异常状态的用例不能从异常变为PASS
    if [ "$(_get_tcstat)" == "$TST_INIT" ]; then
        _set_tcstat $TST_PASS
    fi
}

_tc_fail() {
    if [ "$(_get_tcstat)" != "$TST_FAIL" ]; then
        echo "the testcase first fail here"
    fi
    _set_tcstat $TST_FAIL
}

_tc_setup_common() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0

    # 对用例执行环境进行初始化设置
    mkdir -p "${TST_TC_SYSDIR}/core"
    ulimit -c >"$TST_TC_SYSDIR/old.ulimit.c"
    ulimit -c unlimited
    cat /proc/sys/kernel/core_pattern >"$TST_TC_SYSDIR/old.proc.core_pattern"
    is_root && echo "${TST_TC_SYSDIR}/core/core-e%e-p%p-i%i-s%s-g%g-u%u-t%t.dump" >/proc/sys/kernel/core_pattern

    if is_function tc_setup_common; then
        msg "try call tc_setup_common"
        if tc_setup_common "$@"; then
            msg "call tc_setup_common success"
        else
            err "call tc_setup_common fail"
            _tst_ret=1
        fi
    else
        msg "tc_setup_common not define"
    fi

    return $_tst_ret
}

_tc_setup() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0
    touch "$TST_TC_SYSDIR/tc_setup_called"
    if is_function tc_setup; then
        msg "try call tc_setup"
        if tc_setup "$@"; then
            msg "call tc_setup success"
        else
            err "call tc_setup fail"
            _tst_ret=1
        fi
    else
        msg "tc_setup not define"
    fi

    return $_tst_ret
}

_do_test() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0
    if is_function do_test; then
        msg "try call do_test"
        if do_test "$@"; then
            msg "call do_test success"
        else
            err "call do_test fail"
            _tst_ret=1
        fi
    else
        err "do_test not define"
        _tst_ret=1
    fi

    return $_tst_ret
}

_tc_teardown() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0
    if is_function tc_teardown; then
        msg "try call tc_teardown"
        if tc_teardown "$@"; then
            msg "call tc_teardown success"
        else
            err "call tc_teardown fail"
            _tst_ret=1
        fi
    else
        msg "tc_teardown not define"
    fi

    return $_tst_ret
}

_tc_teardown_common() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0
    if is_function tc_teardown_common; then
        msg "try call tc_teardown_common"
        if tc_teardown_common "$@"; then
            msg "call tc_teardown_common success"
        else
            err "call tc_teardown_common fail"
            _tst_ret=1
        fi
    else
        msg "tc_teardown_common not define"
    fi

    # 恢复用例执行前做的初始化设置
    ulimit -c "$(cat "$TST_TC_SYSDIR/old.ulimit.c")"
    is_root && cat "$TST_TC_SYSDIR/old.proc.core_pattern" >/proc/sys/kernel/core_pattern

    return $_tst_ret
}

_set_ts_setup_stat() {
    echo "$1" >"$TST_TS_SYSDIR/ts.setup.stat"
}

_get_ts_setup_stat() {
    cat "$TST_TS_SYSDIR/ts.setup.stat" 2>/dev/null
}

_is_ts_setup_called() {
    test -f "$TST_TS_SYSDIR/ts.setup.stat"
}

_clean_ts_setup_stat() {
    rm -rf "$TST_TS_SYSDIR/ts.setup.stat"
}
_tc_trap_call() {
    _tc_run_complete || exit 1
    exit 0
}

# Func: 设置用例超时时间
# Args:
#   $1 -- 超时时间，0表示不超时
_tc_timeout() {
    # 有非法字符则不超时
    if ! echo "$1" | grep -q "^[0-9]\+$"; then
        return
    fi
    # 超时时间为0则不超时
    if [ "$1" -eq 0 ]; then
        return
    fi

    local _tst_timeout="$1"
    local _sleep_count=0
    while [ $_sleep_count -le "$_tst_timeout" ]; do
        # 用例主进程退出就不用等待超时了
        [ -d "/proc/$TST_TC_PID" ] || return
        _sleep_count=$((_sleep_count + 1))
        sleep 1
    done

    [ -d "/proc/$TST_TC_PID" ] || return
    local _tst_stat
    _tst_stat="$(_get_tcstat)"
    if [ "$_tst_stat" != "$TST_FAIL" ]; then
        _set_tcstat $TST_TIMEOUT
        _assert_trap_exit
    fi
}

_tc_run_complete() {
    local _tst_ret=0

    # 只有用例的主进程才能执行此函数
    [ "$TST_TC_PID" != "$$" ] && return 0
    # 为了避免死循环，此函数只能进入一次
    if [ -e "$TST_TC_SYSDIR/_tc_run_complete.flag" ]; then
        return
    fi
    touch "$TST_TC_SYSDIR/_tc_run_complete.flag"
    if [ -e "$TST_TC_SYSDIR/tc_setup_called" ]; then
        if _tc_teardown "$@"; then
            msg "call _tc_teardown success"
        else
            err "call _tc_teardown fail"
            _tst_ret=1
        fi
    else
        msg "the tc_setup not called, so tc_teardown ignore"
    fi
    if _tc_teardown_common "$@"; then
        msg "call _tc_teardown_common success"
    else
        err "call _tc_teardown_common fail"
        _tst_ret=1
    fi

    # TCase自动化执行框架需要用这个输出判断用例是否执行完
    echo "Global test environment tear-down"
    # 判断用例是否超时了
    if [ -e "$TST_TC_SYSDIR/timeout.flag" ]; then
        local _tst_stat
        _tst_stat="$(_get_tcstat)"
        if [ "$_tst_stat" != "$TST_FAIL" ] && [ "$_tst_stat" != "$TST_MANUAL" ] &&
            [ "$_tst_stat" != "$TST_SKIP" ]; then
            echo 11
        fi
    fi
    # 用例失败则收集系统日志上传到TCase用于定位
    case "$(_get_tcstat)" in
        "$TST_PASS")
            msg "RESULT : $TST_TC_NAME ==> [  PASSED  ]"
            msg "testcase execute complete and pass"
            echo "case-result: PASS" >>"$TST_RESULT_FILE"
            ;;
        "$TST_FAIL")
            msg "testcase execute complete but fail"
            msg "RESULT : $TST_TC_NAME ==> [  FAILED  ]"
            _tst_ret=1
            echo "case-result: FAIL" >>"$TST_RESULT_FILE"
            ;;
        "$TST_INIT")
            msg "testcase execute not complete or no assertions during testing"
            msg "RESULT : $TST_TC_NAME ==> [  NOTEST  ]"
            _tst_ret=1
            echo "case-result: ABORT" >>"$TST_RESULT_FILE"
            ;;
        "$TST_SKIP")
            msg "testcase prerequisites are not met"
            msg "RESULT : $TST_TC_NAME ==> [  SKIP  ]"
            _tst_ret=0
            echo "case-result: SKIP" >>"$TST_RESULT_FILE"
            ;;
        "$TST_MANUAL")
            msg "this is a manual testcase, need to be executed manually"
            msg "RESULT : $TST_TC_NAME ==> [  MANUAL  ]"
            _tst_ret=0
            echo "case-result: MANUAL" >>"$TST_RESULT_FILE"
            ;;
        "$TST_TIMEOUT")
            msg "testcase execution timeout"
            msg "RESULT : $TST_TC_NAME ==> [  TIMEOUT  ]"
            _tst_ret=1
            echo "case-result: TIMEOUT" >>"$TST_RESULT_FILE"
            ;;
        *)
            msg "testcase execute encountered unknown error"
            msg "RESULT : $TST_TC_NAME ==> [  UNKNOWN  ]"
            _tst_ret=1
            echo "case-result: ABORT" >>"$TST_RESULT_FILE"
            ;;
    esac
    local _tst_case_end
    _tst_case_end=$(get_up_time_ms)
    msg "cost $(diff_time_ms2sec "$g_tst_case_start" "$_tst_case_end")"

    {
        echo "case-end-time: $(get_timestamp_ms)"
        echo ""
    } >>"$TST_RESULT_FILE"

    rm -rf "$TST_TC_SYSDIR"
    return $_tst_ret
}

tst_main() {
    local _tst_ret=0

    if [ -z "$TST_TS_TOPDIR" ]; then
        msg "the TST_TS_TOPDIR not set"
        return 1
    fi
    if [ ! -d "$TST_TS_TOPDIR" ]; then
        msg "the TST_TS_TOPDIR=$TST_TS_TOPDIR not dir"
        return 1
    fi

    [ -f "$TST_TS_SYSDIR/environment_variable" ] && source "$TST_TS_SYSDIR/environment_variable"
    export TST_TC_PID=$$
    TST_TC_CWD="$(realpath "$(dirname "$0")")"
    export TST_TC_CWD
    TST_TC_FILE_FULL="$(realpath "$0")"
    export TST_TC_FILE_FULL
    TST_TC_FILE="${TST_TC_FILE_FULL#${TST_TS_TOPDIR}/}"
    export TST_TC_FILE
    TST_TC_NAME="$(_get_case_attr "用例名称")"
    export TST_TC_NAME
    export TST_TC_SYSDIR="$TST_TS_TOPDIR/logs/testcase/.tc.${TST_TC_PID}.sysdir"
    TST_TC_TIMEOUT="$(_get_case_attr "超时时间")"
    export TST_TC_TIMEOUT

    mkdir -p "$TST_TC_SYSDIR"
    _set_tcstat $TST_INIT
    _shou_case_attr
    # 如果是手动用例，直接设置结果状态为MANUAL，脚本文件的内容还是继续执行
    local _is_auto
    _is_auto="$(_get_case_attr "自动化")"
    if [ "$_is_auto" == "0" ]; then
        msg "this is a manual testcase, set the result to MANUAL"
        _set_tcstat $TST_MANUAL
    fi

    # 用于断言，有的断言会终止测试活动
    trap _tc_trap_call "$g_tst_trap_signal"
    # 设置超时
    _tc_timeout "$TST_TC_TIMEOUT" &

    # 生成结果记录文件
    _get_tst_result_file

    {
        echo "TESTCASE"
        echo "case-name: $TST_TC_NAME"
        echo "case-id: $(_get_case_attr "用例ID")"
        echo "case-type: case-type"
        echo "case-level: $(_get_case_attr "用例级别")"
        echo "case-label: $(_get_case_attr "用例标签")"
        echo "case-steps: $(_get_case_attr "测试步骤")"
        echo "case-result-id: ${TST_TC_NAME}-result"
        echo "case-start-time: $(get_timestamp_ms)"
    } >>"$TST_RESULT_FILE"

    if _is_ts_setup_called; then
        msg "tsuite setup executed, stat is $(_get_ts_setup_stat)"
    else
        msg "tsuite setup may not executed"
    fi

    cd "$TST_TC_CWD" || return 1

    if _tc_setup_common "$@"; then
        if _tc_setup "$@"; then
            msg "call _tc_setup success"
            if _do_test "$@"; then
                msg "call _do_test success"
            else
                err "call _do_test fail"
                _tst_ret=1
            fi
        else
            err "call _tc_setup fail"
            _tst_ret=1
        fi
    else
        err "call _tc_setup_common fail"
        _tst_ret=1
    fi

    _tc_run_complete "$@" || _tst_ret=1

    return $_tst_ret
}
